Feature Engineering com séries de preços de ativos financeiros



Resumo

Este documento apresenta o processo de Feature Engineering aplicado a dados de séries temporais financeiras, contemplando:

  • Download de dados de commodities via yahooquery
  • Cálculo de log-retornos e análise de distribuições (assimetria, histograma)
  • Modelagem GARCH(1,1) para estimar variância condicional
  • Visualização de resultados utilizando timetk no R

📌 Introdução: Feature Engineering em dados de séries financeiras

Medindo a Volatilidade: Com e Sem GARCH

A abordagem tradicional calcula a volatilidade como o desvio padrão dos retornos históricos em uma janela móvel de tamanho \(N\): (dias de negociação)

\[ \sigma_{\text{hist}} = \sqrt{\frac{1}{N-1}\sum_{i=1}^{N}(r_i - \bar{r})^2} \]

Vantagens:

  • Simplicidade e facilidade de implementação.

Desvantagens:

  • Assume volatilidade constante durante a janela. (ou seja precisa de um range dias e não é capaz de medir o desvio ou o risco/volatilidade de um dia pro outro.)

  • Não capta a persistência dos choques (efeito de clustering).

  • Não reage dinamicamente a choques recentes.

Volatilidade com o GARCH

O modelo GARCH(1,1) estima a variância condicional de forma dinâmica:

\[ \sigma_t^2 = \omega + \alpha \epsilon_{t-1}^2 + \beta \sigma_{t-1}^2. \]

Onde: - \(\epsilon_{t-1}^2\) reflete o impacto dos choques recentes. - \(\sigma_{t-1}^2\) reflete a persistência da volatilidade do período anterior. - \(\omega > 0\), \(\alpha \geq 0\) e \(\beta \geq 0\) são parâmetros estimados.

A soma \(\alpha + \beta\) mede a persistência total da volatilidade:

  • Próximo de 1: Choques têm efeitos duradouros; a volatilidade permanece alta por vários períodos.

  • Menor que 1: Os choques se dissipam mais rapidamente; a volatilidade retorna ao seu nível médio mais rápido.

Vantagens do GARCH:

  • Modela a volatilidade de forma dinâmica.

  • Captura o efeito de “clustering” dos choques.

  • Permite previsões mais precisas da volatilidade futura.

Desvantagens do GARCH:

  • Requer estimação de parâmetros e pressupõe uma estrutura específica para a volatilidade.

  • Pode ser sensível à escolha da distribuição dos resíduos (por exemplo, normal vs. \(t\) de Student).

    A volatilidade histórica pode ser medida como o desvio-padrão dos log-retornos calculado em uma janela móvel de \(N\) dias de negociação.

    Neste exemplo, adotamos uma janela móvel de 5 dias (aproximadamente uma semana de negociação. Para um mês de negociação, utilizar 22 e um ano 252) para capturar a volatilidade diária dos ativos.

    Vantagens:

    • Simplicidade e facilidade de implementação.

    Desvantagens:

    • Assume volatilidade constante durante a janela.

    • Não capta a persistência dos choques.

    • Não reage dinamicamente a choques recentes.

    Code
    # Calcular a volatilidade histórica com uma janela móvel de 5 dias
    
    # Supondo que 'log_returns' já foi calculado e possui a coluna 'date' e os log-retornos dos ativos
    window = 5
    
    # Cria um DataFrame para armazenar a volatilidade histórica
    vol_hist = pd.DataFrame({'date': log_returns["date"]})
    
    # Calcula o desvio-padrão móvel (volatilidade) para cada ativo
    for col in log_returns.columns[1:]:
        vol_hist[col] = log_returns[col].rolling(window=window).std()
    
    # Exibe as ultimas linhas do DataFrame de volatilidade histórica
    #print(vol_hist.head()) # 5 primeiros serão NaN
    print(vol_hist.tail())
                date  BEEF3.SA  BRFS3.SA       GIS       HRL  JBSS3.SA  MRFG3.SA  \
    1239  2025-04-04  0.018061  0.017195  0.019901  0.019937  0.013475  0.017031   
    1240  2025-04-07  0.017915  0.017151  0.020406  0.020119  0.012847  0.017031   
    1241  2025-04-08  0.016138  0.007259  0.024098  0.021751  0.008219  0.004705   
    1242  2025-04-09  0.042623  0.038677  0.028590  0.024068  0.023526  0.025228   
    1243  2025-04-10  0.040566  0.038906  0.021658  0.016153  0.022935  0.023309   
    
               TSN  
    1239  0.032352  
    1240  0.027654  
    1241  0.027724  
    1242  0.037064  
    1243  0.036158  

    Plotando temos:

    Code
    # Retire o comando #| eval: false pra conseguir executar essa celula dentro do Quarto
    # Certificar que "date" é datetime (já feito)
    vol_hist['date'] = pd.to_datetime(vol_hist['date'])
    
    # Transformar para formato longo
    vol_hist_long = vol_hist.melt(id_vars='date', var_name='Ativo', value_name='Volatilidade_Hist')
    
    fig = px.line(vol_hist_long, x='date', y='Volatilidade_Hist', color='Ativo',
                  title='Volatilidade Histórica (Desvio-Padrão) com janela de 5 dias')
    fig.show()

    O desvio-padrão assume que precisaremos de um range maior do que 2 pontos no tempo, o que limita nossa análise pois a incompatibiliza, uma vez que precisaremos comparar os riscos diários x retornos diários (como feito anteriormente). Ou seja, não podemos comparar retornos dia-a-dia x volatilidades (risco) de 5 em 5 dias p. ex.

    Os modelos heterocedásticos (da família ARCH) estimam a variância condicional dos nossos dados, ou seja, em linguagem de finanças, eles são capazes de capturar as volatilidades ou risco dos retornos dos preços de ativos financeiros ponto a ponto no tempo, ou seja, dia a dia.

    O modelo GARCH(1,1) com distribuição \(t\) assimétrica não está disponível diretamente na maioria das bibliotecas Python. No entanto, podemos utilizar um GARCH(1,1) com uma distribuição \(t\) padrão para estimar a variância condicional. O modelo é representado por:

    \[ r_t = \mu + \epsilon_t \]

    \[ \epsilon_t = \sigma_t z_t, \quad z_t \sim t_{\nu}(0, 1) \]

    \[ \sigma_t^2 = \omega + \alpha \epsilon_{t-1}^2 + \beta \sigma_{t-1}^2 \]

    Onde:

    • \(r_t\) é o log-retorno no tempo \(t\).
    • \(\mu\) é a média dos retornos.
    • \(\epsilon_t\) é o termo de erro, condicionado às informações passadas.
    • \(\sigma_t^2\) é a variância condicional no tempo \(t\).
    • \(\omega, \alpha, \beta\) são os parâmetros a serem estimados, com \(\omega > 0, \alpha \geq 0, \beta \geq 0\).
    • \(z_t\) segue uma distribuição \(t\) de Student com \(ν\) graus de liberdade para capturar as caudas pesadas observadas em retornos financeiros.

    A soma \(\alpha + \beta\) é frequentemente utilizada para medir a persistência da volatilidade: quanto mais próximos de 1, maior a persistência dos choques na volatilidade.

    Vamos estimar a variância condicional (\(\sigma^2_{t}\) ) para cada ativo:

    Code
    # Estimar o modelo GARCH(1,1) e salvar variância condicional
    var_condicional = pd.DataFrame({"date": log_returns["date"]})
    
    for col in log_returns.columns[1:]:
        am = arch_model(log_returns[col], vol="Garch", p=1, q=1, dist="t")
        res = am.fit(disp="off")
        var_condicional[col] = res.conditional_volatility ** 2
    
    var_condicional.head()
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0007186. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0009502. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0001738. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0002101. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0004777. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0008154. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0002983. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    date BEEF3.SA BRFS3.SA GIS HRL JBSS3.SA MRFG3.SA TSN
    1 2020-04-14 0.006489 0.003739 0.000189 0.002342 0.000879 0.001129 0.002966
    2 2020-04-15 0.005795 0.003823 0.000202 0.002844 0.000781 0.000866 0.002396
    3 2020-04-16 0.005688 0.003715 0.000179 0.002033 0.000779 0.000740 0.002656
    4 2020-04-17 0.007190 0.003715 0.000222 0.002954 0.000672 0.000705 0.002138
    5 2020-04-20 0.006506 0.003712 0.000190 0.002054 0.000642 0.000673 0.002299

    Vamos avaliar os parâmetros estimados do modelo:

    Code
    # Inferir sobre os parâmetros do modelo GARCH(1,1) para cada ativo do portfólio
    
    params_list = []
    
    # Iterar sobre cada ativo (exceto a coluna 'date')
    for col in log_returns.columns[1:]:
        am = arch_model(log_returns[col], vol="Garch", p=1, q=1, dist="t")
        res = am.fit(disp="off")
    
        par = res.params
        alpha_val = par.get("alpha[1]", None)
        beta_val  = par.get("beta[1]", None)
        alpha_beta_sum = (alpha_val if alpha_val is not None else 0) + (beta_val if beta_val is not None else 0)
    
        # Interpretação curta
        if alpha_beta_sum >= 0.9:
            interp = f"Alta persistência (α+β = {alpha_beta_sum:.4f})."
        else:
            interp = f"Baixa/moderada persistência (α+β = {alpha_beta_sum:.4f})."
    
        params_list.append({
             "Ativo": col,
             "mu": par.get("mu", None),
             "omega": par.get("omega", None),
             "alpha": alpha_val,
             "beta": beta_val,
             "alpha+beta": alpha_beta_sum,
             "nu": par.get("nu", None),
             "Interpretacao": interp
        })
    
    garch_params = pd.DataFrame(params_list)
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0007186. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0009502. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0001738. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0002101. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0004777. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0008154. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    
    C:\Users\kuiav\anaconda3\Lib\site-packages\arch\univariate\base.py:309: DataScaleWarning: y is poorly scaled, which may affect convergence of the optimizer when
    estimating the model parameters. The scale of y is 0.0002983. Parameter
    estimation work better when this value is between 1 and 1000. The recommended
    rescaling is 100 * y.
    
    This warning can be disabled by either rescaling y before initializing the
    model or by setting rescale=False.
    

    A soma \(\alpha + \beta\) é um indicador crucial na modelagem GARCH para avaliar a persistência da volatilidade. Em termos práticos, os parâmetros \(\alpha\) e \(\beta\) têm funções distintas:

    • \(\alpha\): Representa o impacto dos choques recentes (a inovação ou termo de erro \(\epsilon_{t-1}^2\) na volatilidade atual. Um valor mais alto de \(\alpha\) indica que choques recentes têm um efeito maior em aumentar a volatilidade.
    • \(\beta\): Captura a persistência da volatilidade ao longo do tempo, ou seja, o efeito da volatilidade passada (\(\sigma_{t-1}^2)\) sobre a volatilidade presente. Valores maiores de \(\beta\) sugerem que a volatilidade tende a se manter elevada por um período mais longo.

    Quando somamos esses dois parâmetros, ou seja, quando calculamos \(\alpha + \beta\), obtemos uma medida da persistência total da volatilidade:

    • Se \(\alpha + \beta\) estiver próximo de 1, isso indica que os choques que afetam a volatilidade têm efeitos de longa duração. Em outras palavras, um choque na volatilidade tem um impacto que se dissipa muito lentamente, mantendo a volatilidade elevada por vários períodos.
    • Se \(\alpha + \beta\) for significativamente menor que 1, os efeitos dos choques são de curta duração e a volatilidade retorna rapidamente ao seu nível médio após um impacto.

    Em alguns casos, quando \(\alpha + \beta = 1\), o modelo é denominado IGARCH (Integrated GARCH), o que implica que os choques têm efeitos persistentes permanentemente, ou seja, a volatilidade não reverte para um valor médio fixo.

    Esta característica é particularmente importante na análise de séries financeiras, pois a persistência alta da volatilidade pode implicar maior risco de mercado e desafios na previsão dos retornos futuros. Assim, a soma \(\alpha + \beta\) serve como uma medida de “memória” dos choques, indicando se a volatilidade reage de forma passageira ou duradoura a eventos inesperados.

    Graficamente temos:

    Code
    # Retire o comando #| eval: false pra conseguir executar essa celula dentro do Quarto
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots
    
    # Para o ativo "ZC=F"
    returns_zc = log_returns[['date', 'BRFS3.SA']].copy()
    vol_zc = var_condicional[['date', 'BRFS3.SA']].copy()
    
    # Converter "date" para datetime, se necessário
    returns_zc['date'] = pd.to_datetime(returns_zc['date'])
    vol_zc['date'] = pd.to_datetime(vol_zc['date'])
    
    # Criar figura com dois subplots compartilhando o eixo x
    fig = make_subplots(
        rows=2, cols=1,
        shared_xaxes=True,
        vertical_spacing=0.05,
        subplot_titles=("Retornos Diários - BRFS3.SA", "Volatilidade Condicional (GARCH) - BRFS3.SA")
    )
    
    # Adicionar o gráfico de retornos
    fig.add_trace(
        go.Scatter(x=returns_zc['date'], y=returns_zc['BRFS3.SA'], mode='lines', name='Retornos'),
        row=1, col=1
    )
    
    # Adicionar o gráfico de volatilidade condicional
    fig.add_trace(
        go.Scatter(x=vol_zc['date'], y=vol_zc['BRFS3.SA'], mode='lines', name='Volatilidade'),
        row=2, col=1
    )
    
    fig.update_layout(
        height=600,
        width=900,
        title_text="Retorno vs. Volatilidade (GARCH) - BRFS3.SA",
        xaxis2_title="Data",
        yaxis1_title="Retorno",
        yaxis2_title="Volatilidade Condicional"
    )
    
    fig.show()

    Em alguns casos, a variância condicional pode apresentar grandes oscilações se houver outliers nos retornos ou problemas de convergência do modelo. Verifique:

    • Qualidade e limpeza dos dados
    • Resumo do ajuste (parâmetros \(\alpha,\beta\) plausíveis?)
    • Distribuição (\(t\) vs. normal)
    • Modelos alternativos (EGARCH, GJR-GARCH, etc.)

    Aqui iremos visualizar o comportamento do ativo ZC=F, futuros de milho:

    Code
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots
    
    # Para o ativo "ZC=F"
    returns_zc = log_returns[['date', 'BRFS3.SA']].copy()
    vol_zc = var_condicional[['date', 'BRFS3.SA']].copy()
    
    # Converter "date" para datetime, se necessário
    returns_zc['date'] = pd.to_datetime(returns_zc['date'])
    vol_zc['date'] = pd.to_datetime(vol_zc['date'])
    
    # Criar figura com dois subplots compartilhando o eixo x
    fig = make_subplots(
        rows=2, cols=1,
        shared_xaxes=True,
        vertical_spacing=0.05,
        subplot_titles=("Retornos Diários - BRFS3.SA", "Volatilidade Condicional (GARCH) - BRFS3.SA")
    )
    
    # Adicionar o gráfico de retornos
    fig.add_trace(
        go.Scatter(x=returns_zc['date'], y=returns_zc['BRFS3.SA'], mode='lines', name='Retornos'),
        row=1, col=1
    )
    
    # Adicionar o gráfico de volatilidade condicional
    fig.add_trace(
        go.Scatter(x=vol_zc['date'], y=vol_zc['BRFS3.SA'], mode='lines', name='Volatilidade'),
        row=2, col=1
    )
    
    fig.update_layout(
        height=600,
        width=900,
        title_text="Retorno vs. Volatilidade (GARCH) - BRFS3.SA",
        xaxis2_title="Data",
        yaxis1_title="Retorno",
        yaxis2_title="Volatilidade Condicional"
    )
    
    fig.show()

    Notem como o GARCH(1,1) captura bem os picos/quebras e na volatilidade dos retornos.

 


References


Gujarati, D. N., & Porter, D. C. (2009). Basic econometrics (5th ed.). McGraw-Hill.

Hyndman, R.J., & Athanasopoulos, G. (2021) Forecasting: principles and practice, 3rd edition, OTexts: Melbourne, Australia. OTexts.com/fpp3. Accessed on march 2025.